export class Despatcher {
  constructor(element) {
    this.element = element
  }
  dispatch(type, properties) {
    const event = new Event(type)
    for (let name in properties) {
      event[name] = properties[name]
    }
    this.element.dispatchEvent(event)
  }
}

export class Linstener {
  constructor(element, recognizer) {

    const contexts = new Map()
    let isLinsteningMouse = false
    element.addEventListener('mousedown', event => {
      const context = Object.create(null)
      contexts.set('mouse' + (1 << event.button), context)
      recognizer.start(event, context)

      const mousemove = event => {
        let button = 1
        while(button <= event.buttons) {
          if (button & event.buttons) {
            // order of buttons & button property is not same
            let key = null
            if (button === 2) {
              key = 4
            } else if (button === 4) {
              key = 2
            } else {
              key = button
            }
            const context = contexts.get('mouse' + key)
            recognizer.move(event, context)
          }
          button = button << 1
        }
      }
      const mouseup = event => {
        const context = contexts.get('mouse' + (1 << event.button))
        recognizer.end(event, context)
        contexts.delete('mouse' + (1 << event.button))

        if (event.buttons === 0) {
          document.removeEventListener('mousemove', mousemove)
          document.removeEventListener('mouseup', mouseup)
          isLinsteningMouse = false
        }
      }
      if (!isLinsteningMouse) {
        document.addEventListener('mousemove', mousemove)
        document.addEventListener('mouseup', mouseup)
        isLinsteningMouse = true
      }
    })

    element.addEventListener('touchstart', event => {
      for (let touch of event.changedTouches) {
        const context = Object.create(null)
        contexts.set(touch.identifier, context)
        recognizer.start(touch, context)
      }
    })
    element.addEventListener('touchmove', event => {
      for (let touch of event.changedTouches) {
        const context = contexts.get(touch.identifier)
        recognizer.move(touch, context)
      }
    })
    element.addEventListener('touchend', event => {
      for (let touch of event.changedTouches) {
        const context = contexts.get(touch.identifier)
        recognizer.end(touch, context)
        contexts.delete(touch.identifier)
      }
    })
    element.addEventListener('touchcancel', event => {
      for (let touch of event.changedTouches) {
        const context = contexts.get(touch.identifier)
        recognizer.cancel(touch, context)
        contexts.delete(touch.identifier)
      }
    })

  }
}
export class Recognizer {
  constructor(dispatcher) {
    this.dispatcher = dispatcher
  }
  start({clientX, clientY}, context) {
    context.startX = clientX
    context.startY = clientY
    context.isTap = true
    context.isPan = false
    context.isPress = false

    context.points = [{
      t: Date.now(),
      x: clientX,
      y: clientY
    }]

    context.handler = setTimeout(() => {
      context.isTap = false
      context.isPan = false
      context.isPress = true
      context.handler = null
      this.dispatcher.dispatch('press', {})
    }, 500)
  }
  move({clientX, clientY}, context) {
    const dx = clientX - context.startX
    const dy = clientY - context.startY
    if (!context.isPan && dx ** 2 + dy ** 2 > 100) {
      context.isTap = false
      context.isPan = true
      context.isPress = false
      context.isVertical = Math.abs(dx) < Math.abs(dy)
      this.dispatcher.dispatch('panstart', {
        startX: context.startX,
        startY: context.startY,
        isVertical: context.isVertical,
        clientX,
        clientY,
      })
      clearTimeout(context.handler)
    }
    if (context.isPan) {
      this.dispatcher.dispatch('pan', {
        startX: context.startX,
        startY: context.startY,
        isVertical: context.isVertical,
        clientX,
        clientY,
      })
    }
    context.points = context.points.filter(point => Date.now() - point.t < 500)
    context.points.push({
      t: Date.now(),
      x: clientX,
      y: clientY
    })
  }
  end({clientX, clientY}, context) {
    if (context.isTap) {
      this.dispatcher.dispatch('tap', {})
      clearTimeout(context.handler)
    }
    if (context.isPress) {
      this.dispatcher.dispatch('pressend', {})
    }
    context.points = context.points.filter(point => Date.now() - point.t < 500)
    let d = null
    let v = null
    if (!context.points.length) {
      v = 0
    } else {
      d = Math.sqrt((clientX - context.points[0].x) ** 2 + (clientY - context.points[0].y) ** 2)
      v = d / (Date.now() - context.points[0].t)
    }
    if (v > 1.5) {
      context.isFlick = true
      this.dispatcher.dispatch('flick', {
        startX: context.startX,
        startY: context.startY,
        isVertical: context.isVertical,
        isFlick: context.isFlick,
        velocity: v,
        clientX,
        clientY,
      })
    } else {
      context.isFlick = false
    }

    if (context.isPan) {
      this.dispatcher.dispatch('panend', {
        startX: context.startX,
        startY: context.startY,
        isVertical: context.isVertical,
        isFlick: context.isFlick,
        clientX,
        clientY,
      })
    }
  }
  cancel ({clientX, clientY}, context) {
    clearTimeout(context.handler)
    this.dispatcher.dispatch('flick', {})
  }
}

export function enableGesture(element) {
  new Linstener(element, new Recognizer(new Despatcher(element)))
}